一文教你玩转 Apache Doris 分区分桶新功能|新版本揭秘
数据分片(Sharding)是分布式数据库分而治之 (Divide And Conquer) 这一设计思想的体现。过去的单机数据库在大数据量下往往面临存储和 IO 的限制,而分布式数据库则通过数据划分的规则,将数据打散分布至不同的机器或节点上,形成分布式存储,因此突破了单机存储空间和 IO 的瓶颈、使库表数据量可以无限拓展。
数据分片主要有范围分片或哈希分片这两种方式,而在实际数据库的实现中,往往呈现为分区和分桶两种形式。分区一般是按照时间或其他连续值对数据进行划分,在执行查询操作时可以通过分区裁剪过滤不必要的范围扫描,提升执行效率,同时也使得对分区数据的增删改等管理操作更为便捷。而分桶则是按照某个关键字执行哈希运算,将相同哈希值的数据放到一起,这样可以有效定位数据、避免数据倾斜。
在 Apache Doris 中,同样也遵从一定的数据分布规则。数据以关系表(Table)的形式进行呈现,会依次按照先分区(Partition)、再分桶(Bucket)的方式划分,最终在同一个分桶中的数据会形成数据分片(Tablet)。Tablet 是 Apache Doris 中多副本高可用、集群间数据调度与均衡的最小物理存储单位。
# 现状与问题
在 Doris 中,分区与分桶是如何创建的?我们以一个网站站点的建表实例说明分区与分桶的创建方式,该网站的站点建表语句如下:
-- 该表记录了某个时间点,在某个站点上各个用户的pv数据
CREATE TABLE demo.test_tbl(
sdate DATE, -- 日期
site INT, -- 站点id
city VARCHAR(64), -- 城市
user VARCHAR(32) DEFAULT '', -- 用户名
pv BIGINT -- pv量
) ENGINE=olap DUPLICATE KEY(sdate, site, city)
[PARTITION_DESC]
[BUCKET_DESC]
PROPERTIES ("replication_num" = "1");
其中 [PARTITION_DESC] 表示创建分区的详细语句,[BUCKET_DESC] 表示创建分桶的语句。
创建分区
Apache Doris 支持两种分区形式,List Partition 与 Range Partition。
List Partition 相当于对分区的列值进行枚举,因此选择的分区列最好是有区分度的可枚举值,例如本例中的 city。根据 city 列的枚举值创建多个 List Partition,则 PARTITION_DESC
可以写为:
-- 以city作为分区列,创建华北、东北、华中、西南等分区
PARTITION BY LIST(city)
(
PARTITION `p_huabei` VALUES IN ("beijing", "tianjin", "shijiazhuang"),
PARTITION `p_dongbei` VALUES IN ("shenyang", "dalian"),
PARTITION `p_huazhong` VALUES IN ("wuhan", "changsha")
PARTITION `p_xinan` VALUES IN ("chengdu", "chongqing")
)
Range Partition
- 静态 Range Partition
PARTITION_DESC
可以写为:-- 以sdate这个时间列作为分区列,
-- 日期处于[min, 2023-01-01)的数据,都放到名为p2022的分区下;
-- 日期处于[2023-01-01, 2023-01-02)的数据,都放到名为p20230101的分区下;
-- 日期处于[2023-01-02, 9999-12-31)的数据,都放到名为pmax的分区下;
PARTITION BY RANGE(sdate)
(
PARTITION `p2022` VALUES LESS THAN ("2023-01-01"),
PARTITION `p20230101` VALUES LESS THAN ("2023-01-02"),
PARTITION `pmax` VALUES LESS THAN ("9999-12-31")
)
- 动态 Range Partition
PARTITION BY RANGE(sdate)()
PARTITION
进行配置:PROPERTIES (
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.start" = "-30",
"dynamic_partition.end" = "3",
"dynamic_partition.prefix" = "p",
"dynamic_partition.create_history_partition"="true",
"replication_num" = "1"
);
创建分桶
分桶在物理层面即数据分片(Tablet)。在数据表完成分区后,指定部分列作为分桶列,将这些列数据中相同哈希值的数据合到一起,形成了 Tablet。一个表中 Tablet 总数量 = 分区数(Partition num)x 分桶数(Bucket num)x 数据副本数(Replication_num)。
[BUCKET_DESC] 语句非常简单,只需要一句:
DISTRIBUTED BY HASH(site) BUCKETS 20
不足与思考
分区数量过多的情况下,使用 List Partition 或者静态 Range Partition 会使得 SQL 较为繁琐,编写起来费时费力; 若是使用动态 Range Partition,则需要掌握多个参数,使用方式不友好且学习成本较高;而当存在大量历史冷数据来说,动态 Range Partition 只能指定单一粒度,无法灵活组合不同的分区粒度; 分桶个数的设置十分依赖用户对 Apache Doris 数据分布机制和业务数据本身的理解,使用门槛较高。不合理的分桶设置将对系统性能和稳定性造成一定程度冲击:分桶数太多将导致单个 Tablet 的数据量过小,数据聚合效果不佳、查询性能不能得到有效发挥,并且元数据管理压力大;个数太少则单个 Tablet 包含的数据量过大,不利于副本的迁移、补齐,且会增加 Schema Change 或者 Rollup 操作失败重试的代价。
# 批量分区与Auto Bucket的设计与实现
批量创建分区
批量创建分区功能在前期充分调研了用户的需求,本着简洁、强大、易用的设计目标,将设计核心锁定在几个要素中:
时间区间范围(会考虑开闭问题) 时间跨度(即每个分区的时间维度的大小) 时间单位(年、月、日、时、周等)
PARTITION_DESC
只需要一句,并且不用在PARTITION
中设置分区相关参数:-- 当然,分区创建个数受到max_multi_partition_num参数控制,该值默认为4096,有需求可以修改
PARTITION BY RANGE(sdate)
(
FROM ("2013-01-01") TO ("2023-01-01") INTERVAL 1 DAY
)从这个 case 来看,批量分区功能的语法更为简洁,但该功能的易用性和灵活性远不止于此。
从这个 case 来看,批量分区功能的语法更为简洁,但该功能的易用性和灵活性远不止于此。
假设有另一批数据:公司前几年的数据量较大且为冷数据,故可以将一年的数据合到一个分区里面;而后来因为业务迅速发展,需要将每一月的数据作为一个分区;随着公司业务进一步发展,按月分区已经不能满足快速增长的数据需求,需要按周进行分区;……;时至今日,公司每天产生海量数据,可能需要按小时分区才能符合需求。根据这个场景,不难写出批量分区创建的 PARTITION_DESC:
-- 此处需要注意,如果要使用小时级别的分区,则分区列必须是datetime类型
-- 同样的,分区创建个数也受到max_multi_partition_num参数控制
PARTITION BY RANGE(sdate)
(
FROM ("2000-01-01") TO ("2021-01-01") INTERVAL 1 YEAR,
FROM ("2021-01-01") TO ("2022-01-01") INTERVAL 1 MONTH,
FROM ("2022-01-01") TO ("2023-01-01") INTERVAL 1 WEEK,
FROM ("2023-01-01") TO ("2023-02-01") INTERVAL 1 DAY,
FROM ("2023-02-01 00") TO ("2099-12-31 23") INTERVAL 1 HOUR
)
PARTITION BY RANGE(sdate)
(
PARTITION pold VALUES LESS THAN ("2022-01-01"),
FROM ("2022-01-01") TO ("2023-01-01") INTERVAL 1 DAY
)
Auto Bucket 自动分桶推算
BUCKET_DESC
非常简单,但是需要指定分桶个数;而在自动分桶推算功能上,BUCKET_DESC
的语法直接将分桶数改成"Auto",并新增一个 Properties 配置即可:-- 旧版本指定分桶个数的创建语法
DISTRIBUTED BY HASH(site) BUCKETS 20
-- 新版本使用自动分桶推算的创建语法
DISTRIBUTED BY HASH(site) BUCKETS AUTO
properties("estimate_partition_size" = "100G")
estimate_partition_size
表示一个单分区的数据量。该参数是可选的,如果没有给出则 Doris 会将 estimate_partition_size 的默认值取为 10GB。若是整体数据量较小,则分桶数不要设置过多 若是整体数据量较大,则应使桶数跟总的磁盘块数相关,充分利用每台 BE 机器和每块磁盘的能力
初始分桶推算
1. 先根据数据量得出一个桶数 N。首先使用 estimate_partition_size 的值除以 5(按文本格式存入 Doris 中有 5 比 1 的数据压缩比计算),得到的结果为:
< 100MB,则取 N=1 < 1GB,则取 N=2 >= 1GB,则每一个 GB 一个分桶
先计算一个中间值 x = min(M, N, 128), 如果 x < N并且x < BE节点个数,则最终分桶为 y 即 BE 节点个数;否则最终分桶数为 x
int N = 计算N值;
int M = 计算M值;
int y = BE节点个数;
int x = min(M, N, 128);
if (x < N && x < y) {
return y;
}
return x;
有了上述算法,咱们再引入一些例子来更好地理解这部分逻辑:
case 1:
数据量 100 MB,10 台 BE 机器,2TB * 3 块盘
数据量 N = 1
BE 磁盘 M = 10 * (2TB/50GB) * 3 = 1230
x = min(M, N, 128) = 1
case 2:
case 3:
可以看到,详细逻辑与原则是匹配的。
后续分桶推算
estimate_partition_size
进行评估。此时计算分桶有两种计算方式,假设以天来分区,往前数第一天分区大小为 S7,往前数第二天分区大小为 S6,依次类推到 S1;如果 7 天内的分区数据每日严格递增,则此时会取趋势值
有6个delta值,分别是 S7 - S6 = delta1, S6 - S5 = delta2, ... S2 - S1 = delta6 由此得到平均的delta值: avg_delta = (delta1 + delta2 + ... + delta6) / 6 = (S7 - S1) / 6 那么,今天的estimate_partition_size = S7 + avg_delta
非第一种的情况,此时直接取前几天的 EMA 平均值
今天的 estimate_partition_size = EMA(S1, ..., S7)
根据上述算法,初始分桶个数以及后续分桶个数都能被计算出来。跟之前只能指定固定分桶数不同,由于业务数据的变化,有可能前面分区的分桶数和后面分区的分桶数不一样,这对用户是透明的,用户无需关心每一分区具体的分桶数是多少,而这一自动推算的功能会让分桶数更加合理。
自动分桶推算功能 PR:https://github.com/apache/doris/pull/15250
效果
select * from test_tbl where sdate = "2020-03-23" and site = 1
# 总结
本文引用
作者介绍:
SelectDB 官网:
Apache Doris 官网:
Apache Doris Github:
- End-
欢迎更多的开源技术爱好者加入 Apache Doris 社区交流群,携手成长,共建社区生态。Apache Doris 社区当前已容纳了上万名开发者和使用者,承载了 30+ 交流社群,如果你也是 Apache Doris 的爱好者,非常欢迎您的加入!